<?php
/*
 * @Author: 李红雨 - RainLee <rainlee1990@yeah.net>
 * @Date: 2022-04-02 14:50:37
 * @LastEditors: 李红雨 - RainLee <rainlee1990@yeah.net>
 * @LastEditTime: 2022-04-14 17:36:35
 * @Description: Auth权限策略处理类
 */

//数据库
/*
-- ----------------------------
-- sys_users 用户表
-- ----------------------------
DROP TABLE IF EXISTS `sys_users`;
CREATE TABLE `sys_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(80) NOT NULL COMMENT '用户邮箱',
  `mobile` char(11) NOT NULL COMMENT '手机号',
  `password` varchar(255) NOT NULL COMMENT '用户密码',
  `username` varchar(15) NOT NULL DEFAULT '' COMMENT '用户名',
  `group_ids` varchar(255) NOT NULL DEFAULT '' COMMENT '用户组',
  `remember_token` char(60) DEFAULT NULL COMMENT '记住我',
  `last_login_ip` int(11) DEFAULT '0' COMMENT '最后登录IP',
  `last_login_time` int(11) DEFAULT '0' COMMENT '最后登录时间',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(1:正常,0:禁用)',
  `create_at` int(11) NOT NULL COMMENT '创建时间',
  `delete_at` int(11) DEFAULT NULL COMMENT '删除时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`),
  UNIQUE KEY `mobile` (`mobile`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- sys_auth_rules，规则表，
-- id:主键，node：规则唯一标识(就是常见的路由列表，如：admin/index/index), title：规则中文名称,例如添加商品 status 状态：为1正常，为0禁用，condition：规则表达式，为空表示存在就验证，不为空表示按照条件验证
-- ----------------------------
 DROP TABLE IF EXISTS `sys_auth_rules`;
CREATE TABLE `sys_auth_rules` (
  `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `pid` int(11) NOT NULL DEFAULT '0' COMMENT '父节点索引',
  `title` char(20) NOT NULL DEFAULT '' COMMENT '标题',
  `node` char(80) NOT NULL DEFAULT '' COMMENT '权限标识',
  `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '类型(0:目录,1:菜单,2:按钮)',
  `condition` char(100) NOT NULL DEFAULT '' COMMENT '认证条件',
  `name` varchar(80) NOT NULL DEFAULT '' COMMENT '路由名称',
  `path` varchar(255) NOT NULL DEFAULT '' COMMENT '路由标识',
  `component` varchar(255) NOT NULL DEFAULT '' COMMENT '组件路径',
  `icon` varchar(255) NOT NULL COMMENT '图标',
  `is_url` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否超链接',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:正常)',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='权限规则表';
-- ----------------------------
-- sys_auth_groups 用户组表，
-- id：主键， title:用户组中文名称， rules：用户组拥有的规则id， 多个规则","隔开，status 状态：为1正常，为0禁用
-- ----------------------------
DROP TABLE IF EXISTS `sys_auth_groups`;
CREATE TABLE `sys_auth_groups` (
  `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `title` char(100) NOT NULL DEFAULT '' COMMENT '角色名',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(1:正常,0:禁用)',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='角色组表';
-- ----------------------------
-- sys_auth_group_rule 用户组权限节点关联表
-- gid:用户组ID，rule_id:权限节点ID
-- ----------------------------
DROP TABLE IF EXISTS `sys_auth_group_rule`;
CREATE TABLE `sys_auth_group_rule` (
  `gid` mediumint(8) unsigned NOT NULL COMMENT '角色索引',
  `rule_id` mediumint(8) unsigned NOT NULL COMMENT '权限索引',
  UNIQUE KEY `gid_rule_id` (`gid`,`rule_id`),
  KEY `gid` (`gid`),
  KEY `rule_id` (`rule_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='角色权限关联表';
 */

namespace rainlee\auth;

use think\facade\Config;
use think\facade\Db;
use think\facade\Request;
use think\facade\Session;

class Policies
{

    protected $guard;

    protected $user;
    //默认配置
    protected $config = [];

    protected $rules = [];

    /**
     * 类架构函数
     * Auth constructor.
     */
    public function __construct($name, Authenticatable $user)
    {
        $this->guard = $name;
        $this->user = $user;
        $this->config = Config::get('auth.policies.' . $name);
    }

    /**
     * 检查权限
     * @param string|array $name 需要验证的规则列表,支持逗号分隔的权限规则或索引数组
     * @param string  $mode 执行check的模式
     * @param string  $relation 如果为 'or' 表示满足任一条规则即通过验证;如果为 'and'则表示需满足所有规则才能通过验证
     * return bool               通过验证返回true;失败返回false
     */
    public function check($name, $mode = 'url', $relation = 'or')
    {
        if (
            !$this->config['auth_on'] ||
            (isset($this->config['super_admin']) && $this->user->id == $this->config['super_admin'])
        ) {
            return true;
        }
        if (is_string($name)) {
            $name = strtolower($name);
            if (strpos($name, ',') !== false) {
                $name = explode(',', $name);
            } else {
                $name = [$name];
            }
        }
        // 过滤无需认证的节点
        $name = array_diff($name, $this->config['ignored']);
        if (empty($name)) {
            return true;
        }

        $list = []; //保存验证通过的规则名
        if ('url' == $mode) {
            $REQUEST = unserialize(strtolower(serialize(Request::param())));
        }

        // 获取用户需要验证的所有有效规则列表
        $authList = $this->getAuthListForUser($this->user);
        foreach ($authList as $auth) {
            $query = preg_replace('/^.+\?/U', '', $auth);
            if ('url' == $mode && $query != $auth) {
                parse_str($query, $param); //解析规则中的param
                $intersect = array_intersect_assoc($REQUEST, $param);
                $auth = preg_replace('/\?.*$/U', '', $auth);
                if (in_array($auth, $name) && $intersect == $param) {
                    //如果节点相符且url参数满足
                    $list[] = $auth;
                }
            } else {
                if (in_array($auth, $name)) {
                    $list[] = $auth;
                }
            }
        }
        if ('or' == $relation && !empty($list)) {
            return true;
        }
        $diff = array_diff($name, $list);
        if ('and' == $relation && empty($diff)) {
            return true;
        }
        return false;
    }

    /**
     * 获取用户组权限
     * 
     * @param string|array $gids
     * @return array
     */
    protected function getRulesForGroups($gids)
    {
        if (is_string($gids)) {
            $gids = explode(',', $gids);
        }
        $map = [['status', '=', 1]];
        if (
            $this->config['auth_on'] &&
            (!isset($this->config['super_admin']) || $this->user->id != $this->config['super_admin'])
        ) {
            // 转换表名
            $auth_group_rule = $this->config['group_rule'];
            // 执行查询
            $group_rule_ids = Db::name($auth_group_rule)->where('gid', 'in', $gids)->column('rule_id');
            $group_rule_ids = array_unique($group_rule_ids);
            //读取用户组所有权限规则
            $map[] = ['id', 'in', $group_rule_ids];
        }
        $this->rules = Db::name($this->config['rules'])->where($map)->select()->toArray();
        return $this->rules;
    }

    public function getRules()
    {
        // 读取用户有权限规则
        return empty($this->rules) ? $this->getRulesForGroups($this->user->group_ids) : $this->rules;
    }

    /**
     * 获得当前登录用户可用的权限列表
     * @param array $type
     * @return array
     */
    protected function getAuthListForUser()
    {
        static $_authList = []; //保存用户验证通过的权限列表
        if (isset($_authList[$this->user->getAuthIdentifier()])) {
            return $_authList[$this->user->getAuthIdentifier()];
        }
        if (2 == $this->config['type'] && Session::has("_{$this->guard}_auth_list_" . $this->user->getAuthIdentifier())) {
            return Session::get("_{$this->guard}_auth_list_" . $this->user->getAuthIdentifier());
        }

        // 读取用户有权限规则
        $rules = $this->getRules();

        //循环规则，判断结果。
        $authList = []; //
        foreach ($rules as $rule) {
            if (empty($rule['node'])) continue;
            if (!empty($rule['condition'])) {
                //根据condition进行验证
                $command = preg_replace('/\{(\w*?)\}/', '$this->user->\\1', $rule['condition']);
                @(eval('$condition=(' . $command . ');'));
                if (isset($condition) && $condition) {
                    $authList[] = strtolower($rule['node']);
                }
            } else {
                //只要存在就记录
                $authList[] = strtolower($rule['node']);
            }
        }
        $_authList[$this->user->getAuthIdentifier()] = $authList;
        if (2 == $this->config['type']) {
            //规则列表结果保存到session
            Session::set("_{$this->guard}_auth_list_" . $this->user->getAuthIdentifier(), $authList);
        }
        return array_unique($authList);
    }
}
